﻿//-----------------------------------------------------------------------
// <copyright file="EntityBase.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Scene.Entity
{
    using System;
    using System.Runtime.CompilerServices;
    using TileTool.Common;
    using TileTool.Common.Math;
    using TileTool.DataBag;
    using TileTool.Render;
    using TileTool.Scene;
    using TileTool.Scene.Collision;
    using TileTool.Settings;

    /// <summary>
    /// Common base class implementation for all entities in the editor.
    /// </summary>
    public class EntityBase : IEntity
    {
        #region Fields - Private

        /// <summary>
        /// Name of the entity.
        /// </summary>
        private DataBagValue<string> name;

        /// <summary>
        /// GUID of the entity.
        /// </summary>
        private Guid guid = Guid.Empty;

        /// <summary>
        /// Entity GUID.
        /// </summary>
        private DataBagValue<string> guidDataBag;

        /// <summary>
        /// Matrix for the entity.
        /// </summary>
        private DataBagValue<Matrix2D> mat;

        /// <summary>
        /// Matrix for the entity in parent space.
        /// </summary>
        private DataBagValue<Matrix2D> parentSpaceMat;

        /// <summary>
        /// Size of the entity.
        /// </summary>
        private DataBagValue<Size2D> size;

        /// <summary>
        /// Color of the entity.
        /// </summary>
        private DataBagValue<ColorRGBA> color;

        /// <summary>
        /// Flags for the entity.
        /// </summary>
        private uint flags = (uint)EntityFlags.None;

        /// <summary>
        /// Scene the entity is currently in.
        /// </summary>
        private ISceneManager scene = null;

        /// <summary>
        /// Parent entity for this entity.
        /// </summary>
        private IEntity parent = null;

        /// <summary>
        /// Parent entity GUID.
        /// </summary>
        private DataBagValue<string> parentEntityGuid;

        /// <summary>
        /// Data bag for all entity data.
        /// </summary>
        private DataBag bag = null;

        /// <summary>
        /// Child entities for this entity.
        /// </summary>
        private IEntityCollection children = new EntityCollection();

        /// <summary>
        /// Child entity we are currently adding / removing.
        /// </summary>
        private IEntity currentAddRemoveChild = null;

        #endregion

        #region Constructor

        /// <summary>
        /// Entity constructor.
        /// </summary>
        /// <param name="createData">Creation data for the entity.</param>
        public EntityBase(EntityCreateData createData)
        {
            if (createData == null)
            {
                throw new ArgumentNullException("createData", "Create data must be non-null.");
            }

            createData.Validate();

            IDataBag clone = createData.Bag.Clone();
            if (clone as DataBag == null)
            {
                throw new ArgumentException("Create data Bag field must be a DataBag.");
            }

            this.bag = (DataBag)clone;

            this.SetupDataBagVariables();
            this.SetupGuid();
        }

        #endregion

        #region Events - Public

        /// <summary>
        /// Event triggered when the entity changes.
        /// </summary>
        public event EntityChanged Changed;

        #endregion

        #region Enumerations - Private

        /// <summary>
        /// Flags for the entity.
        /// </summary>
        [Flags]
        private enum EntityFlags : uint
        {
            /// <summary>
            /// No flags are set.
            /// </summary>
            None = 0x00000000,

            /// <summary>
            /// Set if the entity is selected.
            /// </summary>
            Selected = 0x00000001,

            /// <summary>
            /// Set if the entity is visible.
            /// </summary>
            Visible = 0x00000002,

            /// <summary>
            /// Set if the entity is frozen.
            /// </summary>
            Frozen = 0x00000004,

            /// <summary>
            /// Set if any descendant of the entity is selected.
            /// </summary>
            DescendantSelected = 0x00000008,
        }

        #endregion

        #region Properties - Public - General

        /// <summary>
        /// Gets or sets the name of the entity.
        /// </summary>
        public string Name
        {
            get
            {
                return this.name.Value;
            }

            set
            {
                this.name.Value = value;
                this.TriggerChanged(EntityChangedProperty.Name, this.name);
            }
        }

        /// <summary>
        /// Gets the GUID of the entity.
        /// </summary>
        public Guid Guid
        {
            get
            {
                return this.guid;
            }

            private set
            {
                this.guid = value;
                this.guidDataBag.Value = this.guid.ToString();
            }
        }

        /// <summary>
        /// Gets or sets the color of the entity.
        /// </summary>
        public ColorRGBA Color
        {
            get
            {
                return this.color.Value;
            }

            set
            {
                this.color.Value = value;
                this.TriggerChanged(EntityChangedProperty.Color, this.color);
            }
        }

        /// <summary>
        /// Gets the data bag for the object.
        /// 
        /// The data bag represents the model of the object and as the object's
        /// position or matrix change, the database should be updated as well.
        /// 
        /// All object data should be stored in the data bag.
        /// </summary>
        public IDataBag Bag
        {
            get
            {
                return this.bag;
            }
        }

        #endregion

        #region Properties - Public - Position / Orientation / Size

        /// <summary>
        /// Gets if this entity can be sized by a manipulation tool.
        /// </summary>
        public virtual bool CanSize
        {
            get
            {
                return true;
            }
        }

        /// <summary>
        /// Gets the position of the entity.
        /// </summary>
        public Vector2D Pos
        {
            get
            {
                return this.mat.Value.Pos;
            }

            set
            {
                Matrix2D newMat = this.Matrix;

                newMat.Pos = value;

                this.mat.Value = newMat;

                this.OnMatrixChanged();

                this.TriggerChanged(EntityChangedProperty.Matrix, this.mat);
            }
        }

        /// <summary>
        /// Gets or sets the matrix associated with the entity.
        /// </summary>
        public Matrix2D Matrix
        {
            get
            {
                return this.mat.Value;
            }

            set
            {
                this.mat.Value = value;

                this.OnMatrixChanged();

                this.TriggerChanged(EntityChangedProperty.Matrix, this.mat);
            }
        }

        /// <summary>
        /// Gets the matrix of the entity in parent space.
        /// </summary>
        public Matrix2D ParentMatrix
        {
            get
            {
                return this.parentSpaceMat.Value;
            }

            set
            {
                this.parentSpaceMat.Value = value;

                this.TriggerChanged(EntityChangedProperty.ParentMatrix, this.parentSpaceMat);
            }
        }

        /// <summary>
        /// Gets or sets the size of the entity.
        /// </summary>
        public Size2D Size
        {
            get
            {
                return this.size.Value;
            }

            set
            {
                float w = MathUtility.Clamp(value.Width, EntityUtility.MinSize, EntityUtility.MaxSize);
                float h = MathUtility.Clamp(value.Height, EntityUtility.MinSize, EntityUtility.MaxSize);

                this.size.Value = new Size2D(w, h);

                this.TriggerChanged(EntityChangedProperty.Size, this.size);
            }
        }

        #endregion

        #region Properties - Public - Object state

        /// <summary>
        /// Gets or sets if the scene the entity is currently is in. null means
        /// the entity is not in a scene.
        /// </summary>
        public ISceneManager Scene
        {
            get
            {
                return this.scene;
            }

            set
            {
                this.scene = value;
            }
        }

        /// <summary>
        /// Gets the parent entity of this entity. Can be null.
        /// </summary>
        public IEntity Parent
        {
            get
            {
                return this.parent;
            }

            set
            {
                if (this.parent == value)
                {
                    return;
                }

                if (this.parent != null)
                {
                    this.parent.RemoveChild(this);
                }

                this.parent = value;
                
                if (this.parent != null)
                {
                    value.AddChild(this);

                    this.parentEntityGuid.Value = this.Parent.Guid.ToString();
                }

                this.TriggerChanged(EntityChangedProperty.Parent);
            }
        }

        /// <summary>
        /// Gets a read only collection which contains the children for this entity.
        /// </summary>
        public IEntityCollection Children
        {
            get
            {
                return new ReadOnlyEntityCollection(this.children);
            }
        }

        /// <summary>
        /// Gets or sets if the entity is selected.
        /// </summary>
        public bool Selected
        {
            get
            {
                return FlagUtility.IsSet(this.flags, (uint)EntityFlags.Selected);
            }

            set
            {
                if (value == this.Selected)
                {
                    return;
                }

                FlagUtility.Set(value, ref this.flags, (uint)EntityFlags.Selected);

                this.TriggerChanged(EntityChangedProperty.Selected);
            }
        }

        /// <summary>
        /// Gets if any descendants of this entity are selected.
        /// </summary>
        public bool DescendantSelected
        {
            get
            {
                return FlagUtility.IsSet(this.flags, (uint)EntityFlags.DescendantSelected);
            }
        }

        /// <summary>
        /// Gets or sets if the entity is visible and should be drawn.
        /// </summary>
        public bool Visible
        {
            get
            {
                return FlagUtility.IsSet(this.flags, (uint)EntityFlags.Visible);
            }

            set
            {
                FlagUtility.Set(value, ref this.flags, (uint)EntityFlags.Visible);

                this.TriggerChanged(EntityChangedProperty.Visible);
            }
        }

        /// <summary>
        /// Gets or sets if the entity is frozen.
        /// </summary>
        public bool Frozen
        {
            get
            {
                return FlagUtility.IsSet(this.flags, (uint)EntityFlags.Frozen);
            }

            set
            {
                FlagUtility.Set(value, ref this.flags, (uint)EntityFlags.Frozen);

                this.TriggerChanged(EntityChangedProperty.Frozen);
            }
        }

        #endregion

        #region Properties - Protected

        /// <summary>
        /// Gets an editable data bag for the entity.
        /// </summary>
        public DataBag EditableBag
        {
            get
            {
                return this.bag;
            }
        }

        #endregion

        #region Methods - Public - Draw

        /// <summary>
        /// Draw the entity with the passed renderer.
        /// </summary>
        /// <param name="renderData">Rendering data. Always non-null.</param>
        public virtual void Draw(IRenderData renderData)
        {
        }

        /// <summary>
        /// Called to draw the entity if it is selected.
        /// </summary>
        /// <param name="renderData">Rendering data. Always non-null.</param>
        public virtual void DrawSelected(IRenderData renderData)
        {
            ICanvasRenderer renderer = renderData.Renderer;
            EntitySettings settings = SettingsSystem.Get<EntitySettings>();
            Size2D selectionSize = this.Size + new Size2D(settings.SelectionRectSize);
            Vector2D drawPoint = Vector2D.Zero;

            drawPoint.x -= (selectionSize.Width - this.Size.Width) * 0.5f;
            drawPoint.y -= (selectionSize.Height - this.Size.Height) * 0.5f;

            renderer.DrawLineRect(drawPoint, selectionSize, settings.SelectionRectColor, 1.0f);
        }

        #endregion

        #region Methods - Public - Collision

        /// <summary>
        /// Tests if the passed rectangle intersects or contains the rectangle
        /// of this entity.
        /// </summary>
        /// <param name="rect">Rectangle to test against.</param>
        /// <returns>Returns true if there was an intersection.</returns>
        public bool Intersects(Rect2D rect)
        {
            // Array has 4 entries. 2 for input rect, X and Y, and 2 for this object.
            //
            // Index 0 and 1 are input rect axes.
            // Index 2 and 3 are this object.
            Vector2D[] axes = new Vector2D[4];
            Vector2D[] inputPoints = rect.GetPoints();
            Vector2D[] objectPoints = this.GetRectanglePoints();

            axes[0] = Vector2D.AxisX;
            axes[1] = Vector2D.AxisY;
            axes[2] = this.Matrix.X;
            axes[3] = this.Matrix.Y;

            foreach (Vector2D axis in axes)
            {
                MinMax<float> objectProjection = new MinMax<float>(float.MaxValue, float.MinValue);
                foreach (Vector2D pt in objectPoints)
                {
                    objectProjection.Add(axis.Dot(pt));
                }
                
                MinMax<float> requestProjection = new MinMax<float>(float.MaxValue, float.MinValue);
                foreach (Vector2D pt in inputPoints)
                {
                    requestProjection.Add(axis.Dot(pt));
                }

                if (!objectProjection.Overlap(requestProjection))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Handle collision against this entity for the provided collision
        /// request.
        /// 
        /// If the request hits an entity, should return the entity that was hit.
        /// </summary>
        /// <param name="request">Collision request.</param>
        /// <returns>Returns the entity that was hit or null if no entity was hit.</returns>
        public virtual IEntity HandleCollision(CollisionRequest request)
        {
            switch (request.Type)
            {
                case CollisionRequestType.Point:
                    return this.HandlePointCollision((PointCollisionRequest)request);
                case CollisionRequestType.Rect:
                    return this.HandleRectCollision((RectCollisionRequest)request);
            }

            return null;
        }

        #endregion

        #region Methods - Public - Parent Child

        /// <summary>
        /// Add the passed entity as a child to this entity.
        /// </summary>
        /// <param name="entity">Entity to add.</param>
        public virtual void AddChild(IEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity", "Child entity must be non-null.");
            }

            if (this.currentAddRemoveChild != null)
            {
                if (this.currentAddRemoveChild != entity)
                {
                    throw new InvalidOperationException("Adding a child while in the processing of adding another child.");
                }
                else
                {
                    return;
                }
            }

            this.currentAddRemoveChild = entity;

            Matrix2D invMtx = this.Matrix.Invert();
            Matrix2D childParentSpaceMtx = entity.Matrix * invMtx;

            entity.ParentMatrix = childParentSpaceMtx;
            entity.Parent = this;

            this.children.Add(entity);
            this.currentAddRemoveChild = null;

            this.OnChildAdded(entity);
        }

        /// <summary>
        /// Remove the passed entity from this one as a child.
        /// </summary>
        /// <param name="entity">Entity to remove.</param>
        public virtual void RemoveChild(IEntity entity)
        {
            if (entity == null)
            {
                return;
            }

            if (this.currentAddRemoveChild != null)
            {
                if (this.currentAddRemoveChild != entity)
                {
                    throw new InvalidOperationException("Removing a child while in the process of removing another child.");
                }
                else
                {
                    return;
                }
            }

            bool res = this.children.Remove(entity);
            if (!res)
            {
                throw new InvalidOperationException("Attempting to remove a child that does not exist.");
            }

            this.currentAddRemoveChild = entity;

            entity.Parent = null;
            entity.ParentMatrix = Matrix2D.Identity;

            this.currentAddRemoveChild = null;

            this.OnChildRemoved(entity);
        }

        #endregion

        #region Methods - Protected - Collision

        /// <summary>
        /// Handle a point collision request.
        /// 
        /// Default implementation tests for a point inside of the entity rectangle.
        /// </summary>
        /// <param name="request">Collision request.</param>
        /// <returns>Returns the entity that was hit or null if no entity was hit.</returns>
        protected virtual IEntity HandlePointCollision(PointCollisionRequest request)
        {
            Matrix2D invMatrix = this.Matrix.Invert();
            Vector2D testPtLocal = invMatrix * request.Point;

            if ((testPtLocal.x < 0.0f) || (testPtLocal.x > this.Size.Width))
            {
                return null;
            }

            if ((testPtLocal.y < 0.0f) || (testPtLocal.y > this.Size.Height))
            {
                return null;
            }

            return this;
        }

        /// <summary>
        /// Handle a rect collision request.
        /// </summary>
        /// <param name="request">Collision request.</param>
        /// <returns>Returns the entity that was hit or null if no entity was hit.</returns>
        protected virtual IEntity HandleRectCollision(RectCollisionRequest request)
        {
            if (this.Intersects(request.Rect))
            {
                return this;
            }

            return null;
        }

        #endregion

        #region Methods - Protected - Entity change

        /// <summary>
        /// Called when a child entity was added.
        /// </summary>
        /// <param name="child">Child that was added.</param>
        protected virtual void OnChildAdded(IEntity child)
        {
            child.Changed += this.HandleChildChanged;
        }

        /// <summary>
        /// Called when a child entity was removed.
        /// </summary>
        /// <param name="child">Child that was removed.</param>
        protected virtual void OnChildRemoved(IEntity child)
        {
            child.Changed -= this.HandleChildChanged;
        }

        /// <summary>
        /// Handle when one of our children changes.
        /// </summary>
        /// <param name="args">Event argumnets.</param>
        protected virtual void HandleChildChanged(EntityChangedEventArgs args)
        {
            if (args.Property != EntityChangedProperty.Selected &&
                args.Property != EntityChangedProperty.DescendantSelected)
            {
                return;
            }

            this.UpdateDescendantSelected();
        }

        /// <summary>
        /// Called when this entity's matrix changes.
        /// </summary>
        protected virtual void OnMatrixChanged()
        {
            // If we have a parent, update our parent space matrix
            if (this.Parent != null)
            {
                Matrix2D invParent = this.Parent.Matrix.Invert();

                this.ParentMatrix = invParent * this.Matrix;
            }

            // If we have children, update their matrix based on 
            // the parent's new matrix.
            foreach (IEntity child in this.children)
            {
                Matrix2D newMtx = this.Matrix * child.ParentMatrix;

                child.Matrix = newMtx;
            }
        }

        /// <summary>
        /// Trigger the changed event for the entity.
        /// </summary>
        /// <param name="property">Property that changed if it is known.</param>
        /// <param name="value">Optional value that changed.</param>
        /// <param name="name">Name of the property that changed.</param>
        protected void TriggerChanged(
            EntityChangedProperty property = EntityChangedProperty.Unknown,
            IDataBagValue value = null,
            [CallerMemberName] string name = "unknown")
        {
            EntityChanged changed = this.Changed;
            if (changed != null)
            {
                EntityChangedEventArgs args = new EntityChangedEventArgs(this, value, name, property);

                changed(args);
            }
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Called to update our descendant selected flag.
        /// </summary>
        private void UpdateDescendantSelected()
        {
            bool current = this.DescendantSelected;
            bool updated = false;
            IEntityCollection children = this.Children;

            foreach (IEntity entity in children)
            {
                updated |= entity.Selected || entity.DescendantSelected;
                if (updated)
                {
                    break;
                }
            }

            if (updated == current)
            {
                return;
            }

            FlagUtility.Set(updated, ref this.flags, (uint)EntityFlags.DescendantSelected);

            this.TriggerChanged(EntityChangedProperty.DescendantSelected);
        }

        /// <summary>
        /// Setup all data bag variables for the entity.
        /// </summary>
        private void SetupDataBagVariables()
        {
            this.name = this.bag.GetOrCreateValue<string>(EntityDataBagKeys.Name, string.Empty);
            this.mat = this.bag.GetOrCreateValue<Matrix2D>(EntityDataBagKeys.Matrix, Matrix2D.Identity);
            this.parentSpaceMat = this.bag.GetOrCreateValue<Matrix2D>(EntityDataBagKeys.MatrixPS, Matrix2D.Identity);
            this.size = this.bag.GetOrCreateValue<Size2D>(EntityDataBagKeys.Size, new Size2D(EntityUtility.DefaultSize));
            this.color = this.bag.GetOrCreateValue<ColorRGBA>(EntityDataBagKeys.Color, ColorRGBA.White);
            this.guidDataBag = this.bag.GetOrCreateValue<string>(EntityDataBagKeys.Guid, Guid.Empty.ToString());
            this.parentEntityGuid = this.bag.GetOrCreateValue<string>(EntityDataBagKeys.ParentGuid, Guid.Empty.ToString());
        }

        /// <summary>
        /// Setup the entity's GUID.
        /// </summary>
        private void SetupGuid()
        {
            try
            {
                this.Guid = new Guid(this.guidDataBag.Value);
                if (this.Guid == Guid.Empty)
                {
                    this.Guid = Guid.NewGuid();
                }
            }
            catch
            {
                this.Guid = Guid.NewGuid();
            }
        }

        #endregion
    }
}